/*
 * // Bristleback plugin - Copyright (c) 2010 bristleback.googlecode.com
 * // ---------------------------------------------------------------------------
 * // This program is free software; you can redistribute it and/or modify it
 * // under the terms of the GNU Lesser General Public License as published by the
 * // Free Software Foundation; either version 3 of the License, or (at your
 * // option) any later version.
 * // This library is distributed in the hope that it will be useful,
 * // but without any warranty; without even the implied warranty of merchantability
 * // or fitness for a particular purpose.
 * // You should have received a copy of the GNU Lesser General Public License along
 * // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
 * // ---------------------------------------------------------------------------
 */

package pl.bristleback.server.bristle.binding.resolvers;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.jwebsocket.token.Token;
import pl.bristleback.server.bristle.binding.BindingError;
import pl.bristleback.server.bristle.binding.BindingErrorsResolver;
import pl.bristleback.server.bristle.binding.BindingResult;
import pl.bristleback.server.bristle.binding.NestedFieldInformation;
import pl.bristleback.server.bristle.exceptions.BristleRuntimeException;
import pl.bristleback.server.bristle.utils.StringUtil;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Basic implementation of {@link pl.bristleback.server.bristle.binding.resolvers.BindingEngine} interface,
 * binds raw and complex objects, lists, maps (with string as key) and arrays.
 * Takes {@link org.jwebsocket.token.MapToken} token as parameter, which is default token passed by framework to action classes <code>performAction()</code> method.
 * <p/>
 * Created on: 2011-02-15 21:39:09 <br/>
 *
 * @author Wojciech Niemiec
 */
public class DefaultBindingEngine implements BindingEngine {
  private static Logger log = Logger.getLogger(DefaultBindingEngine.class.getName());

  private BindingErrorsResolver errorsResolver = new BindingErrorsResolver();
  private static final String CONTAINER_FORMAT = "{0}" + StringUtil.LEFT_BRACKET + "{1}" + StringUtil.RIGHT_BRACKET;
  private static final int SINGLE_ELEMENT_CONTAINER_SIZE = 1;

  public <T> BindingResult<T> bind(T object, Token token, NestedFieldInformation rootInformation) {
    BindingResult<T> result = new BindingResult<T>(object);
    try {
      Map rootValue = getRoot(token, rootInformation);
      processChildren(result, rootInformation, rootValue);
      return result;
    } catch (ClassCastException e) {
      throw new BristleRuntimeException("Cannot cast an object to another class, cause: " + e.getMessage(), e);
    } catch (Exception e) {
      throw new BristleRuntimeException("Unexpected exception while binding object. cause: " + e.getCause(), e);
    }
  }

  /**
   * Gets token root element for binding operation.
   * If {@link pl.bristleback.server.bristle.binding.Bind} annotation specifies the name(key of token element) of root,
   * search of root value is performed. If there is no Map value with key specified by annotation, main token element is returned.
   *
   * @param token           map token to process.
   * @param rootInformation information about root name and root children.
   * @return root token element.
   */
  private Map getRoot(Token token, NestedFieldInformation rootInformation) {
    Map rootValue = token.getMap();
    Object rootWithPath = token.getObject(rootInformation.getName());
    if (!(rootWithPath instanceof Map)) {
      return rootValue;
    }
    return (Map) rootWithPath;
  }

  private void processChildren(BindingResult parentInstance, NestedFieldInformation parentInformation, Object value) throws InstantiationException, IllegalAccessException, InvocationTargetException {
    if (parentInformation.isList()) {
      processChildrenInList(parentInstance, parentInformation, convertValueToList(value));
    } else if (parentInformation.isArray()) {
      processChildrenInArray(parentInstance, parentInformation, convertValueToList(value));
    } else if (parentInformation.isMap()) {
      processChildrenInMap(parentInstance, parentInformation, (Map) value);
    } else {
      processChildrenInPlainObject(parentInstance, parentInformation, (Map) value);
    }
  }

  private void processChildrenInPlainObject(BindingResult bindingResult, NestedFieldInformation parentInformation, Map value) throws IllegalAccessException, InstantiationException, InvocationTargetException {
    Object parentInstance = bindingResult.getProcessedObject();
    Object childInstance;
    String currentPath = bindingResult.getCurrentPath();
    for (Map.Entry<String, NestedFieldInformation> childEntry : parentInformation.getChildren().entrySet()) {
      NestedFieldInformation childInformation = childEntry.getValue();
      setCurrentPath(bindingResult, currentPath, parentInformation, childInformation);
      Object childValue = value.get(childEntry.getKey());
      if (isEmptyValue(childValue)) {
        errorsResolver.checkAndAddNotNullViolationErrors(bindingResult, childInformation);
      } else {
        if (childInformation.isIntermediary() || childInformation.isContainer()) {
          childInstance = processNestedObject(bindingResult, childInformation, childValue);
        } else {
          childInstance = getRawValue(bindingResult, childInformation, childValue);
        }
        if (childInstance != null) {
          childInformation.getSetter().invoke(parentInstance, childInstance);
        }
      }
    }
  }

  private void setCurrentPath(BindingResult bindingResult, String oldCurrentPath, NestedFieldInformation parentInformation, NestedFieldInformation childInformation) {
    String currentPath;
    if (parentInformation.isContainerInPath()) {
      if (StringUtils.isEmpty(oldCurrentPath)) {
        currentPath = childInformation.getName();
      } else {
        currentPath = oldCurrentPath + StringUtil.DOT + childInformation.getName();
      }
    } else {
      currentPath = childInformation.getFullPathName();
    }
    bindingResult.setCurrentPath(currentPath);
  }

  private Object processNestedObject(BindingResult bindingResult, NestedFieldInformation childInformation, Object childValue) throws InstantiationException, IllegalAccessException, InvocationTargetException {
    Object childInstance = getNestedObjectInstance(childInformation.getFieldClass(), childValue);
    bindingResult.setProcessedObject(childInstance);
    processChildren(bindingResult, childInformation, childValue);
    return childInstance;
  }

  private boolean isEmptyValue(Object childValue) {
    return childValue == null || StringUtils.isBlank(childValue.toString());
  }

  @SuppressWarnings("unchecked")
  private void processChildrenInMap(BindingResult bindingResult, NestedFieldInformation parentInformation, Map map) throws IllegalAccessException, InstantiationException, InvocationTargetException {
    Map parentInstance = (Map) bindingResult.getProcessedObject();
    if (isContainerEmpty(bindingResult, parentInformation, map.size())) {
      return;
    }
    Iterator keys = map.keySet().iterator();
    Object childInstance;
    String currentPath = bindingResult.getCurrentPath();
    while (keys.hasNext()) {
      String key = (String) keys.next();
      Object value = map.get(key);
      bindingResult.setCurrentPath(getCurrentPathInContainer(currentPath, key));
      childInstance = processChildInContainer(bindingResult, parentInformation, value);
      parentInstance.put(key, childInstance);
    }
  }

  private boolean isContainerEmpty(BindingResult bindingResult, NestedFieldInformation parentInformation, int containerSize) {
    if (containerSize == 0) {
      errorsResolver.checkAndAddNotNullViolationErrors(bindingResult, parentInformation.getElementFieldInformation());
      return true;
    }
    return false;
  }

  @SuppressWarnings("unchecked")
  private void processChildrenInList(BindingResult bindingResult, NestedFieldInformation parentInformation, List value) throws IllegalAccessException, InstantiationException, InvocationTargetException {
    List parentList = (List) bindingResult.getProcessedObject();
    if (isContainerEmpty(bindingResult, parentInformation, value.size())) {
      return;
    }
    String currentPath = bindingResult.getCurrentPath();
    for (int i = 0; i < value.size(); i++) {
      bindingResult.setCurrentPath(getCurrentPathInContainer(currentPath, i));
      Object childInstance = processChildInContainer(bindingResult, parentInformation, value.get(i));
      parentList.add(childInstance);
    }
  }

  private void processChildrenInArray(BindingResult bindingResult, NestedFieldInformation parentInformation, List value) throws IllegalAccessException, InstantiationException, InvocationTargetException {
    if (isContainerEmpty(bindingResult, parentInformation, value.size())) {
      return;
    }
    Object processedArray = bindingResult.getProcessedObject();
    if (processedArray.getClass().getComponentType().isPrimitive()) {
      processArrayOfPrimitives(bindingResult, parentInformation, value, processedArray);
    } else {
      processArrayOfObjects(bindingResult, parentInformation, value);
    }
  }

  @SuppressWarnings("unchecked")
  private List convertValueToList(Object value) {
    List valueAsList;
    if (value instanceof List) {
      valueAsList = (List) value;
    } else {
      valueAsList = new ArrayList();
      valueAsList.add(value);
    }
    return valueAsList;
  }

  private void processArrayOfObjects(BindingResult bindingResult, NestedFieldInformation parentInformation, List value) throws InstantiationException, IllegalAccessException, InvocationTargetException {
    Object[] parentInstance = (Object[]) bindingResult.getProcessedObject();
    String currentPath = bindingResult.getCurrentPath();
    for (int i = 0; i < value.size(); i++) {
      bindingResult.setCurrentPath(getCurrentPathInContainer(currentPath, i));
      Object arrayValue = value.get(i);
      Object childInstance = processChildInContainer(bindingResult, parentInformation, arrayValue);
      parentInstance[i] = childInstance;
    }
  }

  private void processArrayOfPrimitives(BindingResult bindingResult, NestedFieldInformation parentInformation, List value, Object processedArray) {
    String currentPath = bindingResult.getCurrentPath();
    int length = Array.getLength(processedArray);
    for (int i = 0; i < length; i++) {
      bindingResult.setCurrentPath(getCurrentPathInContainer(currentPath, i));
      Object elementValue = getRawValue(bindingResult, parentInformation.getElementFieldInformation(), value.get(i));
      if (elementValue != null) {
        Array.set(processedArray, i, elementValue);
      }
    }
  }

  private String getCurrentPathInContainer(String currentPath, Object index) {
    return MessageFormat.format(CONTAINER_FORMAT, currentPath, index);
  }

  private Object processChildInContainer(BindingResult bindingResult, NestedFieldInformation parentInformation, Object value) throws InstantiationException, IllegalAccessException, InvocationTargetException {
    Object childInstance;
    if (parentInformation.isIntermediary()) {
      childInstance = processNestedObject(bindingResult, parentInformation.getElementFieldInformation(), value);
    } else {
      childInstance = getRawValue(bindingResult, parentInformation.getElementFieldInformation(), value);
    }
    return childInstance;
  }

  private Object getRawValue(BindingResult bindingResult, NestedFieldInformation fieldInformation, Object arrayValue) {
    Object value = fieldInformation.getResolver().extractValue(arrayValue);
    if (value == null) {
      // could not resolve value, probably type mismatch error
      BindingError typeMismatchError = BindingError.addTypeMismatchError(bindingResult.getCurrentPath());
      bindingResult.addError(typeMismatchError);
    }
    return value;
  }

  private Object getNestedObjectInstance(Class fieldClass, Object value) throws InstantiationException, IllegalAccessException {
    if (List.class.isAssignableFrom(fieldClass)) {
      return new ArrayList();
    } else if (Map.class.isAssignableFrom(fieldClass)) {
      return new HashMap();
    } else if (fieldClass.isArray()) {
      return Array.newInstance(fieldClass.getComponentType(), getCollectionSize(value));
    }
    return fieldClass.newInstance();
  }

  private int getCollectionSize(Object value) {
    if (value instanceof List) {
      return ((List) value).size();
    } else {
      return SINGLE_ELEMENT_CONTAINER_SIZE;
    }
  }
}
